package com.gmadmin.utils.jwt;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import com.gmadmin.common.Constants;
import com.gmadmin.common.HttpStatus;
import com.gmadmin.exception.SecurityException;
import com.gmadmin.entity.vo.auth.JwtResponse;
import com.gmadmin.entity.vo.user.UserPrincipal;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author zhangliang
 * @version 1.0
 * @date 2021/2/3 15:05
 */
@Component
public class JwtUtil {

    private final JwtConfig jwtConfig;
    private final RedisTemplate<Object, Object> stringRedisTemplate;

    protected static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);

    public JwtUtil(JwtConfig jwtConfig, RedisTemplate<Object, Object> stringRedisTemplate) {
        this.jwtConfig = jwtConfig;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 用户验证通过创建jwt
     *
     * @param authentication 验证对象
     * @param rememberMe     记住我
     * @return 返回
     */
    public JwtResponse createJWT(Authentication authentication, Boolean rememberMe) {
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
        return createJWT(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getComponent(), userPrincipal.getAuthorities());
    }

    /**
     * 创建jwt
     *
     * @param rememberMe  记住我
     * @param id          用户id
     * @param username    用户名
     * @param component   菜单地址
     * @param authorities 角色
     * @return 返回创建jwt
     */
    private JwtResponse createJWT(Boolean rememberMe, Long id, String username, List<String> component, Collection<? extends GrantedAuthority> authorities) {
        Date now = new Date();
        JwtBuilder builder = Jwts.builder().setId(id.toString())
                .setSubject(username)
                //登录日期
                .setIssuedAt(now)
                //签证key
                .signWith(SignatureAlgorithm.HS256, jwtConfig.getKey())
                //存放数据
                .claim("component", component)
                .claim("authorities", authorities);

        //设置过期时间
        Long ttl = Boolean.TRUE.equals(rememberMe) ? jwtConfig.getRemember() : jwtConfig.getTtl();


        builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue()));


        //生成jwt
        String jwt = builder.compact();
        String redisKey = Constants.REDIS_JWT_KEY_PREFIX + username;
        // 将生成的JWT保存至Redis,以及redis过期时间,TimeUnit.MILLISECONDS毫米时间，在这里填错了,选成微秒，导致过期很快，找了很久问题
//        TimeUnit.DAYS          //天
//        TimeUnit.HOURS         //小时
//        TimeUnit.MINUTES       //分钟
//        TimeUnit.SECONDS       //秒
//        TimeUnit.MILLISECONDS  //毫秒
//        TimeUnit.NANOSECONDS   //毫微秒
//        TimeUnit.MICROSECONDS  //微秒
        stringRedisTemplate.opsForValue().set(redisKey, jwt, ttl, TimeUnit.MILLISECONDS);
        logger.debug("redisKey=" + redisKey);
        return new JwtResponse(jwt, ttl);
    }


    /**
     * 解析jwt和验证jwt是否和redis是否存在或一致
     *
     * @param jwt jwt
     * @return 返回
     */
    public Claims parseJWT(String jwt) {
        try {
            Claims claims = Jwts.parser().setSigningKey(jwtConfig.getKey()).parseClaimsJws(jwt).getBody();

            String username = claims.getSubject();
            String redisKey = Constants.REDIS_JWT_KEY_PREFIX + username;

            //校验redis中的JWT是否存在
            Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS);
            if (ObjectUtil.isNull(expire) || expire < 0) {
                logger.debug("redis中不存在jwt");
                throw new SecurityException(HttpStatus.UNAUTHORIZED);
            }

            // 校验redis中的JWT是否与当前的一致，不一致则代表用户已注销/用户在不同设备登录，均代表JWT已过期
            String redisToken = stringRedisTemplate.opsForValue().get(redisKey).toString();
            if (!CharSequenceUtil.equals(jwt, redisToken)) {
                throw new SecurityException(HttpStatus.UNAUTHORIZED);
            }

            return claims;
        } catch (ExpiredJwtException e) {
            logger.error("Token 已过期");
            throw new SecurityException(HttpStatus.UNAUTHORIZED);
        } catch (UnsupportedJwtException e) {
            logger.error("不支持的 Token");
            throw new SecurityException(HttpStatus.TOKEN_PARSE_ERROR);
        } catch (MalformedJwtException e) {
            logger.error("Token 无效");
            throw new SecurityException(HttpStatus.TOKEN_PARSE_ERROR);
        } catch (SignatureException e) {
            logger.error("无效的 Token 签名");
            throw new SecurityException(HttpStatus.TOKEN_PARSE_ERROR);
        } catch (IllegalArgumentException e) {
            logger.error("Token 参数不存在");
            throw new SecurityException(HttpStatus.TOKEN_PARSE_ERROR);
        }
    }

    /**
     * 设置过期请求并清除redis
     *
     * @param request 请求
     */
    public void invalidateJWT(HttpServletRequest request) {
        String jwt = getJwtFromRequest(request);
        logger.debug("jwt=" + jwt);
        String username = getUsernameFromJWT(jwt);

        //从redis中清除jwt
        stringRedisTemplate.delete(Constants.REDIS_JWT_KEY_PREFIX + username);
    }

    /**
     * 从 request 的 header 中获取 JWT
     *
     * @param request 请求
     * @return JWT
     */
    public String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        String b = "Bearer ";
        if (CharSequenceUtil.isNotBlank(bearerToken) && bearerToken.startsWith(b)) {
            return bearerToken.substring(7);
        }
        return null;
    }

    /**
     * 根据 jwt 获取用户名
     *
     * @param jwt JWT
     * @return 用户名
     */
    public String getUsernameFromJWT(String jwt) {
        Claims claims = parseJWT(jwt);
        return claims.getSubject();
    }

}
